<template>
    <MyArticle class="readingNotes-webgl-02" title="WebGL学习笔记之二：绘制和变换三角形" date="2021年02月05日">
        <div class="_article-content">
            <p>描述WebGL如何绘制三角形，使用三角形绘制其他类型的基本图形以及基本的移动、旋转、缩放变换。</p>
            <h3 class="title">绘制多个点</h3>
            <p>构成三维模型的基本单位是<strong>三角形</strong>，再复杂的模型，也是由许多个三角形以及这些三角形的顶点构成的。通过创建更细小和更大量的三角形，可以创建更复杂和更逼真的三维模型。</p>
            <p>WebGL提供<strong>缓冲区对象</strong>，可以一次性地向着色器传入多个顶点的数据。缓冲区对象是WebGL系统中的一块内存区域。</p>
            <canvas ref="canvasRef0" v-show="!img0" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img0" v-show="img0" class="webgl-canvas spec-canvas"/>
            <pre><code>// 定义顶点着色器
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    void main() {
        gl_Position = a_Position;
        gl_PointSize = 10.0;
    }
`
// 设置顶点位置
const n = initVertexBuffers(gl)
if (n &lt; 0) {
    console.log('initVertexBuffers failed')
    return
}
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, n)</code></pre>
            <p>initVertexBuffers函数的任务是创建顶点缓冲区对象，并将多个顶点的数据保存在缓冲区中，然后将缓冲区传给顶点着色器。</p>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
    const n = 3
    // 创建缓冲区
    const buffer = gl.createBuffer()
    if (!buffer) {
        return -1
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
    // 向缓冲区对象中写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
    const aPosition = gl.getAttribLocation(gl.program, 'a_Position')
    // 将缓冲区对象分配给attribute变量
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
    // 连接a_Position变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(aPosition)
    return n
}</code></pre>
            <p>创建或者删除缓冲区对象：</p>
            <pre><code>gl.createBuffer()
gl.deleteBuffer(buffer) // 删除buffer表示的对象</code></pre>
            <p>创建缓冲区之后是将缓冲区对象绑定到WebGl系统中已经存在的目标上，这个目标表示缓冲区对象的用途：</p>
            <pre><code>gl.bindBuffer(target, buffer)
// target参数可以有：
// ① gl.ARRAY_BUFFER（缓冲区对象中包含了顶点的数据）
// ② gl.ELEMENT_ARRAY_BUFFER（缓冲区对象中包含了顶点的索引值）</code></pre>
            <p>不能直接向缓冲区写入数据，而只能向目标写入数据：</p>
            <pre><code>gl.bufferData(target, data, usage)
// target为 gl.ARRAY_BUFFER或者 gl.ELEMENT_ARRAY_BUFFER
// data为写入缓冲区对象的数据
// usage参数可以有：
// ① gl.STATIC_DRAW 只会向缓冲区对象中写入一次数据，但需要绘制很多次
// ② gl.STREAM_DRAW 只会向缓冲区对象中写入一次数据，然后绘制若干次
// ③ gl.DYNAMIC_DRAW 会向缓冲区中多次写入数据，并绘制多次</code></pre>
            <p>类型化数组与Array数组类似，但不支持push和pop方法：</p>
            <pre><code>Int8Array // 8位整型数
UInt8Array // 8位无符号整型数
Int16Array // 16位整型数
UInt16Array // 16位无符号整型数
Int32Array // 32位整型数
UInt32Array // 32位无符号整型数
Float32Array // 单精度32位浮点数
Float64Array // 双精度64位浮点数</code></pre>
            <p>具有以下方法和属性：</p>
            <pre><code>get(index) // 获取第index个元素
set(index, value) // 设置第index个元素的值为value
set(array, offset) // 从offset个元素开始将数组array中的值填充进去
length // 数组的长度
BYTES_PER_ELEMENT // 数组中每个元素所占的字节数</code></pre>
            <p>通过vertexAttribPointer方法将绑定到gl.ARRAY_BUFFER的缓冲区对象分配给attribute变量：</p>
            <pre><code>gl.vertextAttribPointer(location, size, type, normalized, stride, offset)
// location：attribute变量的存储位置
// size：指定缓冲区中每个顶点的分量个数
// type：gl.UNSIGNED_BYTE、gl.SHORT、gl.UNSIGNED_SHORT、gl.INT、gl.UNSIGNED_INT、gl.FLOAT
// normalize：true或者false，表明是否将非浮点数归一化到[0,1]或者[-1,1]区间
// stride：指定相邻两个节点间的字节数，默认为0
// offset：指定缓冲区对象中的偏移量</code></pre>
            <p>通过使用gl.enableVertexAttribArray()方法使顶点着色器能够访问缓冲区内的数据；使用gl.disableVertexAttribArray()来关闭分配。</p>
            <h3 class="title">基本图形的绘制</h3>
            <p>gl.drawArrays()方法：</p>
            <pre><code>gl.drawArrays(mode, first, count)
// mode：gl.POINTS、gl.LINES、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
// first：指定从哪个顶点开始绘制
// count：指定绘制需要用到多少个顶点</code></pre>
            <p><strong>gl.LINES</strong>：一系列单独的线段，绘制在(v0, v1)、(v2, v3)、(v4, v5)...，如果点的个数是奇数，最后一个点将被忽略：</p>
            <canvas ref="canvasRef1" v-show="!img1" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img1" v-show="img1" class="webgl-canvas spec-canvas"/>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5])
    ...
gl.drawArrays(gl.LINES, 0, n)</code></pre>
            <p><strong>gl.LINE_STRIP</strong>：一系列连接的线段，绘制在(v0, v1)、(v1, v2)、(v2, v3)...</p>
            <canvas ref="canvasRef2" v-show="!img2" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img2" v-show="img2" class="webgl-canvas spec-canvas"/>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5])
    ...
gl.drawArrays(gl.LINE_STRIP, 0, n)</code></pre>
            <p><strong>gl.LINE_LOOP</strong>：一系列连接的点，绘制在(v0, v1)、(v1, v2)、(v2, v3)...(vn-1, vn)、(vn, v0)，增加了一条从最后一个点到第一个点的线段：</p>
            <canvas ref="canvasRef3" v-show="!img3" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img3" v-show="img3" class="webgl-canvas spec-canvas"/>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5])
    ...
gl.drawArrays(gl.LINE_LOOP, 0, n)</code></pre>
            <p><strong>gl.TRIANGLES</strong>：一系列单独的三角形，绘制在(v0, v1, v2)、(v3, v4, v5)...，如果点的个数不是3的整数倍，多余的一个或者两个点将被忽略。</p>
            <canvas ref="canvasRef4" v-show="!img4" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img4" v-show="img4" class="webgl-canvas spec-canvas"/>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5])
    ...
gl.drawArrays(gl.TRIANGLES, 0, n)</code></pre>
            <p><strong>gl.TRIANGLE_STRIP</strong>：一系列条带状的三角形，前三个点构成了第一个三角形，从第二个点开始的三个点构成了第二个三角形。绘制在(v0, v1, v2)、(v2, v1, v3)、(v2, v3, v4)...，如果点的个数不是3的整数倍，多余的一个或者两个点将被忽略。</p>
            <canvas ref="canvasRef5" v-show="!img5" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img5" v-show="img5" class="webgl-canvas spec-canvas"/>
            <pre><code>const initVertexBuffers = (gl) => {
    const vertices = new Float32Array([-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5])
    ...
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)</code></pre>
            <p><strong>gl.TRIANGLE_FAN</strong>：一系列三角形构成的类似于扇形的图形，绘制在(v0, v1, v2)，(v0, v2, v3)，(v0, v3, v4)...</p>
            <canvas ref="canvasRef6" v-show="!img6" class="webgl-canvas spec-canvas"></canvas>
            <img :src="img6" v-show="img6" class="webgl-canvas spec-canvas"/>
            <pre><code>// 扇形
const radius = 0.75
const points = []
const delt = 30
const PI = Math.PI
for (let i = 0; i &gl;= 6; i++) {
    points.push(radius * Math.cos(i * delt / 180 * PI))
    points.push(radius * Math.sin(i * delt / 180 * PI))
}
points.unshift(...[0.0, -0.5])
...
gl.drawArrays(gl.TRIANGLE_FAN, 0, n)</code></pre>
            <h3 class="title">移动、旋转和缩放</h3>
            <p>平移之后坐标分量为：</p>
            <pre><code>x' = x + Tx
y' = y + Ty
z' = z + Tz</code></pre>
            <p>将平移距离传入顶点着色器，然后分别在顶点坐标的对应分量上相加，赋值给gl_Position：</p>
            <pre><code>// 顶点着色器
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    uniform vec4 u_Translation;
    void main() {
        gl_Position = a_Position + u_Translation;
        gl_PointSize = 10.0;
    }
`
...
const uTranslation = gl.getUniformLocation(gl.program, 'u_Translation')
gl.uniform4f(uTranslation, tx, ty, tz, 0.0)</code></pre>
            <p>原来的位置：</p>
            <canvas ref="canvasRef7" class="webgl-canvas"></canvas>
            <p>x、y分量各自平移0.25之后的位置：</p>
            <canvas ref="canvasRef8" class="webgl-canvas"></canvas>
            <p>旋转β角度之后坐标分量与原坐标的关系：</p>
            <pre><code>x' = x cosβ - y sinβ
y' = x sinβ + y cosβ
z' = z

// 推算过程：设点P(x,y)与原点的连线夹角为α，到原点的距离为R，旋转β角度之后的坐标为：
x' = cos(α+β) * R = cosα * cosβ * R - sinα * sinβ * R = x * cosβ - y * sinβ
y' = sin(α+β) * R = sinα * cosβ * R + cosα * sinβ * R = y * cosβ + x * sinβ</code></pre>
            <p>先计算出旋转角度的正弦和余弦值，然后再传值进uniform变量中去：</p>
            <pre><code>const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    uniform float u_CosB, u_SinB;
    void main() {
        gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;
        gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;
        gl_Position.z = a_Position.z;
        gl_Position.w = 1.0;
        gl_PointSize = 10.0;
    }
`
...
const radian = Math.PI * angle / 180.0
const cosB = Math.cos(radian)
const sinB = Math.sin(radian)
const uCosB = gl.getUniformLocation(gl.program, 'u_CosB')
gl.uniform1f(uCosB, cosB)
const uSinB = gl.getUniformLocation(gl.program, 'u_SinB')
gl.uniform1f(uSinB, sinB)</code></pre>
            <p>旋转之前：</p>
            <canvas ref="canvasRef9" class="webgl-canvas spec-canvas"></canvas>
            <p>旋转20度之后：</p>
            <canvas ref="canvasRef10" class="webgl-canvas spec-canvas"></canvas>
            <p>对于简单的变换，可以使用数学表达式来实现，但是复杂时，则可以使用矩阵运算：<strong>[x', y', z', w'] = xformMatrix * [x, y, z, w]</strong></p>
            <pre><code>// 旋转矩阵
cosB  -sinB   0   0
sinB   cosB    0   0
  0        0      1    0
  0        0      0    1
</code></pre>
            <p>通过旋转矩阵来旋转：</p>
            <pre><code>// 顶点着色器，矩阵可以直接相乘
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    uniform mat4 u_xformMatrix;
    void main() {
        gl_Position = u_xformMatrix * a_Position;
        gl_PointSize = 10.0;
    }
`
...
const radian = Math.PI * angle / 180.0
const cosB = Math.cos(radian)
const sinB = Math.sin(radian)
const xformMatrix = new Float32Array([ // 按列主序储存矩阵
    cosB, sinB, 0.0, 0.0,
    -sinB, cosB, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
    0.0, 0.0, 0.0, 1.0
])
const uxformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
gl.uniformMatrix4fv(uxformMatrix, false, xformMatrix)</code></pre>
            <canvas ref="canvasRef11" class="webgl-canvas spec-canvas"></canvas>
            <pre><code>gl.uniformMatrix4fv(location, transpose, array)
// 将array表示的4x4矩阵分配给由location指定的uniform变量
// location：uniform变量的储存位置
// transpose：在webgl中固定为false即可
// array：待传输的类型化数组，4x4矩阵按列主序储存在其中</code></pre>
            <p>平移矩阵：</p>
            <pre><code>1     0     0     tx
0     1     0     ty
0     0     1     tz
0     0     0      1</code></pre>
            <p>缩放矩阵：</p>
            <pre><code>Sx     0     0     0
0     Sy     0     0
0      0     Sz     0
0      0     0      1</code></pre>
        </div>
    </MyArticle>
</template>

<script>
import { onMounted, ref } from 'vue'
import draw from './gl02'
import translate01 from './translate01'
import rotate01 from './rotate01'
import rotate02 from './rotate02'
export default {
    setup () {
        // =============================================一次性绘制多个点
        const canvasRef0 = ref(null)
        const canvasRef1 = ref(null)
        const canvasRef2 = ref(null)
        const canvasRef3 = ref(null)
        const canvasRef4 = ref(null)
        const canvasRef5 = ref(null)
        const canvasRef6 = ref(null)
        const img0 = ref(null)
        const img1 = ref(null)
        const img2 = ref(null)
        const img3 = ref(null)
        const img4 = ref(null)
        const img5 = ref(null)
        const img6 = ref(null)
        onMounted(() => {
            img0.value = draw(canvasRef0.value, [0.0, 0.5, -0.5, -0.5, 0.5, -0.5], 3, 'POINTS')
            img1.value = draw(canvasRef1.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5], 4, 'LINES')
            img2.value = draw(canvasRef2.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5], 4, 'LINE_STRIP')
            img3.value = draw(canvasRef3.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5], 4, 'LINE_LOOP')
            img4.value = draw(canvasRef4.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5], 4, 'TRIANGLES')
            img5.value = draw(canvasRef5.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5, 0.5, 0.5], 4, 'TRIANGLE_STRIP')

            // 扇形
            const radius = 0.65
            const points = []
            const delt = 30
            const PI = Math.PI
            for (let i = 0; i <= 6; i++) {
                points.push(radius * Math.cos(i * delt / 180 * PI))
                points.push(radius * Math.sin(i * delt / 180 * PI))
            }
            points.unshift(...[0.0, -0.5])
            img6.value = draw(canvasRef6.value, points, 8, 'TRIANGLE_FAN')
        })
        // =============================================平移
        const canvasRef7 = ref(null)
        const canvasRef8 = ref(null)
        onMounted(() => {
            const tx = 0.25
            const ty = 0.25
            const tz = 0
            translate01(canvasRef7.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5], 4, 0.0, 0.0, 0.0)
            translate01(canvasRef8.value, [-0.5, -0.5, -0.25, 0.5, 0.25, -0.5], 4, tx, ty, tz)
        })
        // =============================================旋转
        const canvasRef9 = ref(null)
        const canvasRef10 = ref(null)
        onMounted(() => {
            rotate01(canvasRef9.value, [-0.5, -0.0, -0.0, 0.5, 0.5, -0.0], 3, 0)
            rotate01(canvasRef10.value, [-0.5, -0.0, -0.0, 0.5, 0.5, -0.0], 3, 20)
        })
        // =============================================旋转矩阵
        const canvasRef11 = ref(null)
        onMounted(() => {
            rotate02(canvasRef11.value, [-0.5, -0.0, -0.0, 0.5, 0.5, -0.0], 3, 20)
        })
        return {
            img0, img1, img2, img3, img4, img5, img6, canvasRef0, canvasRef1, canvasRef2, canvasRef3, canvasRef4, canvasRef5, canvasRef6, canvasRef7, canvasRef8, canvasRef9, canvasRef10, canvasRef11
        }
    }
}
</script>

<style lang="scss">
    @use './style.scss';
</style>
